/*
 * Based on JUEL 2.2.1 code, 2006-2009 Odysseus Software GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.activiti.engine.impl.javax.el;

import java.beans.FeatureDescriptor;
import java.util.Iterator;
import java.util.List;

/**
 * Defines property resolution behavior on instances of java.util.List. This resolver handles base
 * objects of type java.util.List. It accepts any object as a property and coerces that object into
 * an integer index into the list. The resulting value is the value in the list at that index. This
 * resolver can be constructed in read-only mode, which means that isReadOnly will always return
 * true and {@link #setValue(ELContext, Object, Object, Object)} will always throw
 * PropertyNotWritableException. ELResolvers are combined together using {@link CompositeELResolver}
 * s, to define rich semantics for evaluating an expression. See the javadocs for {@link ELResolver}
 * for details.
 */
public class ListELResolver extends ELResolver {
    private final boolean readOnly;

    /**
     * Creates a new read/write ListELResolver.
     */
    public ListELResolver() {
        this(false);
    }

    /**
     * Creates a new ListELResolver whose read-only status is determined by the given parameter.
     *
     * @param readOnly true if this resolver cannot modify lists; false otherwise.
     */
    public ListELResolver(boolean readOnly) {
        this.readOnly = readOnly;
    }

    /**
     * Test whether the given base should be resolved by this ELResolver.
     *
     * @param base     The bean to analyze.
     * @param property The name of the property to analyze. Will be coerced to a String.
     * @return base instanceof List
     */
    private static final boolean isResolvable(Object base) {
        return base instanceof List<?>;
    }

    /**
     * Convert the given property to an index in (list) base.
     *
     * @param base     The bean to analyze.
     * @param property The name of the property to analyze. Will be coerced to a String.
     * @return The index of property in base.
     * @throws IllegalArgumentException  if base property cannot be coerced to an integer.
     * @throws PropertyNotFoundException if base is not null and the computed index is out of bounds for base.
     */
    private static final int toIndex(List<?> base, Object property) {
        int index = 0;
        if (property instanceof Number) {
            index = ((Number) property).intValue();
        } else if (property instanceof String) {
            try {
                index = Integer.valueOf((String) property);
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException("Cannot parse list index: " + property);
            }
        } else if (property instanceof Character) {
            index = ((Character) property).charValue();
        } else if (property instanceof Boolean) {
            index = ((Boolean) property).booleanValue() ? 1 : 0;
        } else {
            throw new IllegalArgumentException("Cannot coerce property to list index: " + property);
        }
        if (base != null && (index < 0 || index >= base.size())) {
            throw new PropertyNotFoundException("List index out of bounds: " + index);
        }
        return index;
    }

    /**
     * If the base object is a list, returns the most general type that this resolver accepts for
     * the property argument. Otherwise, returns null. Assuming the base is a List, this method will
     * always return Integer.class. This is because Lists accept integers as their index.
     *
     * @param context The context of this evaluation.
     * @param base    The list to analyze. Only bases of type List are handled by this resolver.
     * @return null if base is not a List; otherwise Integer.class.
     */
    @Override
    public Class<?> getCommonPropertyType(ELContext context, Object base) {
        return isResolvable(base) ? Integer.class : null;
    }

    /**
     * Always returns null, since there is no reason to iterate through set set of all integers.
     *
     * @param context The context of this evaluation.
     * @param base    The list to analyze. Only bases of type List are handled by this resolver.
     * @return null.
     */
    @Override
    public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
        return null;
    }

    /**
     * If the base object is a list, returns the most general acceptable type for a value in this
     * list. If the base is a List, the propertyResolved property of the ELContext object must be
     * set to true by this resolver, before returning. If this property is not true after this
     * method is called, the caller should ignore the return value. Assuming the base is a List,
     * this method will always return Object.class. This is because Lists accept any object as an
     * element.
     *
     * @param context  The context of this evaluation.
     * @param base     The list to analyze. Only bases of type List are handled by this resolver.
     * @param property The index of the element in the list to return the acceptable type for. Will be
     *                 coerced into an integer, but otherwise ignored by this resolver.
     * @return If the propertyResolved property of ELContext was set to true, then the most general
     * acceptable type; otherwise undefined.
     * @throws PropertyNotFoundException if the given index is out of bounds for this list.
     * @throws IllegalArgumentException  if the property could not be coerced into an integer.
     * @throws NullPointerException      if context is null
     * @throws ELException               if an exception was thrown while performing the property or variable resolution.
     *                                   The thrown exception must be included as the cause property of this exception, if
     *                                   available.
     */
    @Override
    public Class<?> getType(ELContext context, Object base, Object property) {
        if (context == null) {
            throw new NullPointerException("context is null");
        }
        Class<?> result = null;
        if (isResolvable(base)) {
            toIndex((List<?>) base, property);
            result = Object.class;
            context.setPropertyResolved(true);
        }
        return result;
    }

    /**
     * If the base object is a list, returns the value at the given index. The index is specified by
     * the property argument, and coerced into an integer. If the coercion could not be performed,
     * an IllegalArgumentException is thrown. If the index is out of bounds, null is returned. If
     * the base is a List, the propertyResolved property of the ELContext object must be set to true
     * by this resolver, before returning. If this property is not true after this method is called,
     * the caller should ignore the return value.
     *
     * @param context  The context of this evaluation.
     * @param base     The list to analyze. Only bases of type List are handled by this resolver.
     * @param property The index of the element in the list to return the acceptable type for. Will be
     *                 coerced into an integer, but otherwise ignored by this resolver.
     * @return If the propertyResolved property of ELContext was set to true, then the value at the
     * given index or null if the index was out of bounds. Otherwise, undefined.
     * @throws PropertyNotFoundException if the given index is out of bounds for this list.
     * @throws IllegalArgumentException  if the property could not be coerced into an integer.
     * @throws NullPointerException      if context is null
     * @throws ELException               if an exception was thrown while performing the property or variable resolution.
     *                                   The thrown exception must be included as the cause property of this exception, if
     *                                   available.
     */
    @Override
    public Object getValue(ELContext context, Object base, Object property) {
        if (context == null) {
            throw new NullPointerException("context is null");
        }
        Object result = null;
        if (isResolvable(base)) {
            int index = toIndex(null, property);
            List<?> list = (List<?>) base;
            result = index < 0 || index >= list.size() ? null : list.get(index);
            context.setPropertyResolved(true);
        }
        return result;
    }

    /**
     * If the base object is a list, returns whether a call to
     * {@link #setValue(ELContext, Object, Object, Object)} will always fail. If the base is a List,
     * the propertyResolved property of the ELContext object must be set to true by this resolver,
     * before returning. If this property is not true after this method is called, the caller should
     * ignore the return value. If this resolver was constructed in read-only mode, this method will
     * always return true. If a List was created using java.util.Collections.unmodifiableList(List),
     * this method must return true. Unfortunately, there is no Collections API method to detect
     * this. However, an implementation can create a prototype unmodifiable List and query its
     * runtime type to see if it matches the runtime type of the base object as a workaround.
     *
     * @param context  The context of this evaluation.
     * @param base     The list to analyze. Only bases of type List are handled by this resolver.
     * @param property The index of the element in the list to return the acceptable type for. Will be
     *                 coerced into an integer, but otherwise ignored by this resolver.
     * @return If the propertyResolved property of ELContext was set to true, then true if calling
     * the setValue method will always fail or false if it is possible that such a call may
     * succeed; otherwise undefined.
     * @throws PropertyNotFoundException if the given index is out of bounds for this list.
     * @throws IllegalArgumentException  if the property could not be coerced into an integer.
     * @throws NullPointerException      if context is null
     * @throws ELException               if an exception was thrown while performing the property or variable resolution.
     *                                   The thrown exception must be included as the cause property of this exception, if
     *                                   available.
     */
    @Override
    public boolean isReadOnly(ELContext context, Object base, Object property) {
        if (context == null) {
            throw new NullPointerException("context is null");
        }
        if (isResolvable(base)) {
            toIndex((List<?>) base, property);
            context.setPropertyResolved(true);
        }
        return readOnly;
    }

    /**
     * If the base object is a list, attempts to set the value at the given index with the given
     * value. The index is specified by the property argument, and coerced into an integer. If the
     * coercion could not be performed, an IllegalArgumentException is thrown. If the index is out
     * of bounds, a PropertyNotFoundException is thrown. If the base is a List, the propertyResolved
     * property of the ELContext object must be set to true by this resolver, before returning. If
     * this property is not true after this method is called, the caller can safely assume no value
     * was set. If this resolver was constructed in read-only mode, this method will always throw
     * PropertyNotWritableException. If a List was created using
     * java.util.Collections.unmodifiableList(List), this method must throw
     * PropertyNotWritableException. Unfortunately, there is no Collections API method to detect
     * this. However, an implementation can create a prototype unmodifiable List and query its
     * runtime type to see if it matches the runtime type of the base object as a workaround.
     *
     * @param context  The context of this evaluation.
     * @param base     The list to analyze. Only bases of type List are handled by this resolver.
     * @param property The index of the element in the list to return the acceptable type for. Will be
     *                 coerced into an integer, but otherwise ignored by this resolver.
     * @param value    The value to be set at the given index.
     * @throws ClassCastException           if the class of the specified element prevents it from being added to this list.
     * @throws PropertyNotFoundException    if the given index is out of bounds for this list.
     * @throws PropertyNotWritableException if this resolver was constructed in read-only mode, or if the set operation is
     *                                      not supported by the underlying list.
     * @throws IllegalArgumentException     if the property could not be coerced into an integer.
     * @throws NullPointerException         if context is null
     * @throws ELException                  if an exception was thrown while performing the property or variable resolution.
     *                                      The thrown exception must be included as the cause property of this exception, if
     *                                      available.
     */
    @Override
    @SuppressWarnings("unchecked")
    public void setValue(ELContext context, Object base, Object property, Object value) {
        if (context == null) {
            throw new NullPointerException("context is null");
        }
        if (isResolvable(base)) {
            if (readOnly) {
                throw new PropertyNotWritableException("resolver is read-only");
            }
            List list = (List) base;
            int index = toIndex(list, property);
            try {
                list.set(index, value);
            } catch (UnsupportedOperationException e) {
                throw new PropertyNotWritableException(e);
            } catch (ArrayStoreException e) {
                throw new IllegalArgumentException(e);
            }
            context.setPropertyResolved(true);
        }
    }
}
